const Yarn = @This();

pub fn print(
    this: *Printer,
    comptime Writer: type,
    writer: Writer,
) !void {
    // internal for debugging, print the lockfile as custom json
    // limited to debug because we don't want people to rely on this format.
    if (Environment.isDebug) {
        if (std.process.hasEnvVarConstant("JSON")) {
            try std.json.stringify(
                this.lockfile,
                .{
                    .whitespace = .indent_2,
                    .emit_null_optional_fields = true,
                    .emit_nonportable_numbers_as_strings = true,
                },
                writer,
            );
            try writer.writeAll("\n");
            return;
        }
    }

    try writer.writeAll(
        \\# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
        \\# yarn lockfile v1
        \\# bun ./bun.lockb --hash:
    );
    try writer.print(
        " {}\n\n",
        .{this.lockfile.fmtMetaHash()},
    );

    try Yarn.packages(this, Writer, writer);
}

fn packages(
    this: *Printer,
    comptime Writer: type,
    writer: Writer,
) !void {
    var slice = this.lockfile.packages.slice();
    const names: []const String = slice.items(.name);
    const resolved: []const Resolution = slice.items(.resolution);
    const metas: []const Lockfile.Package.Meta = slice.items(.meta);
    if (names.len == 0) return;
    const dependency_lists = slice.items(.dependencies);
    const resolutions_buffer = this.lockfile.buffers.resolutions.items;
    const dependencies_buffer = this.lockfile.buffers.dependencies.items;
    const RequestedVersion = std.HashMap(PackageID, []Dependency.Version, IdentityContext(PackageID), 80);
    var requested_versions = RequestedVersion.init(this.lockfile.allocator);
    const all_requested_versions_buf = try this.lockfile.allocator.alloc(Dependency.Version, resolutions_buffer.len);
    var all_requested_versions = all_requested_versions_buf;
    defer this.lockfile.allocator.free(all_requested_versions_buf);
    const package_count = @as(PackageID, @truncate(names.len));
    var alphabetized_names = try this.lockfile.allocator.alloc(PackageID, package_count - 1);
    defer this.lockfile.allocator.free(alphabetized_names);

    const string_buf = this.lockfile.buffers.string_bytes.items;

    // First, we need to build a map of all requested versions
    // This is so we can print requested versions
    {
        var i: PackageID = 1;
        while (i < package_count) : (i += 1) {
            alphabetized_names[i - 1] = i;

            var resolutions = resolutions_buffer;
            var dependencies = dependencies_buffer;

            var j: PackageID = 0;
            var requested_version_start = all_requested_versions;
            while (std.mem.indexOfScalar(PackageID, resolutions, i)) |k| {
                j += 1;

                all_requested_versions[0] = dependencies[k].version;
                all_requested_versions = all_requested_versions[1..];

                dependencies = dependencies[k + 1 ..];
                resolutions = resolutions[k + 1 ..];
            }

            const dependency_versions = requested_version_start[0..j];
            if (dependency_versions.len > 1) std.sort.insertion(Dependency.Version, dependency_versions, string_buf, Dependency.Version.isLessThanWithTag);
            try requested_versions.put(i, dependency_versions);
        }
    }

    std.sort.pdq(
        PackageID,
        alphabetized_names,
        Lockfile.Package.Alphabetizer{
            .names = names,
            .buf = string_buf,
            .resolutions = resolved,
        },
        Lockfile.Package.Alphabetizer.isAlphabetical,
    );

    // When printing, we start at 1
    for (alphabetized_names) |i| {
        const name = names[i].slice(string_buf);
        const resolution = resolved[i];
        const meta = metas[i];
        const dependencies: []const Dependency = dependency_lists[i].get(dependencies_buffer);
        const version_formatter = resolution.fmt(string_buf, .posix);

        // This prints:
        // "@babel/core@7.9.0":
        {
            try writer.writeAll("\n");
            const dependency_versions = requested_versions.get(i).?;

            // https://github.com/yarnpkg/yarn/blob/158d96dce95313d9a00218302631cd263877d164/src/lockfile/stringify.js#L9
            const always_needs_quote = strings.mustEscapeYAMLString(name);

            var prev_dependency_version: ?Dependency.Version = null;
            var needs_comma = false;
            for (dependency_versions) |*dependency_version| {
                if (needs_comma) {
                    if (prev_dependency_version) |*prev| {
                        if (prev.eql(dependency_version, string_buf, string_buf)) {
                            continue;
                        }
                    }
                    try writer.writeAll(", ");
                    needs_comma = false;
                }
                const version_name = dependency_version.literal.slice(string_buf);
                const needs_quote = always_needs_quote or bun.strings.indexAnyComptime(version_name, " |\t-/!") != null or strings.hasPrefixComptime(version_name, "npm:");

                if (needs_quote) {
                    try writer.writeByte('"');
                }

                try writer.writeAll(name);
                try writer.writeByte('@');
                if (version_name.len == 0) {
                    try std.fmt.format(writer, "^{any}", .{version_formatter});
                } else {
                    try writer.writeAll(version_name);
                }

                if (needs_quote) {
                    try writer.writeByte('"');
                }
                prev_dependency_version = dependency_version.*;
                needs_comma = true;
            }

            try writer.writeAll(":\n");
        }

        {
            try writer.writeAll("  version ");

            // Version is always quoted
            try std.fmt.format(writer, "\"{any}\"\n", .{version_formatter});

            try writer.writeAll("  resolved ");

            const url_formatter = resolution.fmtURL(string_buf);

            // Resolved URL is always quoted
            try std.fmt.format(writer, "\"{any}\"\n", .{url_formatter});

            if (meta.integrity.tag != .unknown) {
                // Integrity is...never quoted?
                try std.fmt.format(writer, "  integrity {any}\n", .{&meta.integrity});
            }

            if (dependencies.len > 0) {
                var behavior: Behavior = .{};
                var dependency_behavior_change_count: u8 = 0;
                for (dependencies) |dep| {
                    if (!dep.behavior.eq(behavior)) {
                        if (dep.behavior.isOptional()) {
                            try writer.writeAll("  optionalDependencies:\n");
                            if (comptime Environment.allow_assert) dependency_behavior_change_count += 1;
                        } else if (dep.behavior.isProd()) {
                            try writer.writeAll("  dependencies:\n");
                            if (comptime Environment.allow_assert) dependency_behavior_change_count += 1;
                        } else if (dep.behavior.isDev()) {
                            try writer.writeAll("  devDependencies:\n");
                            if (comptime Environment.allow_assert) dependency_behavior_change_count += 1;
                        } else {
                            continue;
                        }
                        behavior = dep.behavior;

                        // assert its sorted. debug only because of a bug saving incorrect ordering
                        // of optional dependencies to lockfiles
                        if (comptime Environment.isDebug) assert(dependency_behavior_change_count < 3);
                    }

                    try writer.writeAll("    ");
                    const dependency_name = dep.name.slice(string_buf);

                    const needs_quote = strings.mustEscapeYAMLString(dependency_name);

                    if (needs_quote) {
                        try writer.writeByte('"');
                    }
                    try writer.writeAll(dependency_name);
                    if (needs_quote) {
                        try writer.writeByte('"');
                    }
                    try writer.writeAll(" \"");
                    try writer.writeAll(dep.version.literal.slice(string_buf));
                    try writer.writeAll("\"\n");
                }
            }
        }
    }
}

const std = @import("std");

const bun = @import("bun");
const Environment = bun.Environment;
const IdentityContext = bun.IdentityContext;
const assert = bun.assert;
const strings = bun.strings;
const String = bun.Semver.String;

const PackageID = bun.install.PackageID;
const Resolution = bun.install.Resolution;

const Dependency = bun.install.Dependency;
const Behavior = Dependency.Behavior;

const Lockfile = bun.install.Lockfile;
const Printer = bun.install.Lockfile.Printer;
